'''
Created on Oct 13, 2011

@author: conan
'''
import csv
from utils import  Matrix
#===============================================================================
# Scheduling Problem
#===============================================================================
class SP(object):
    '''
    Class for scheduling problem
    @ivar J: 
    @type J: C{list} of L{Job}
    @ivar M: 
    @type M: C{list} of L{Machine}
    @ivar best: the best schedule
    @type best: C{Schedule}
    @ivar matrix: a table that contains the setup times
    @ivar current: the current schedule
    @type current: L{Schedule}
    @ivar directory: the local directory for the files of the scheduling problem  
    '''

    def __init__(self, jobSrc=None, machineSrc=None):
        #DATA#
        self.J = []
        self.M = []
        Job.total = Machine.total = 0
        
        self.directory = ''
        self.jobSrc = jobSrc
        self.machineSrc = machineSrc  
        self.matrix = None
        
        #SEARCH#
        self.fringe = []
        self.current = None
        self.best = None
        self._cost = None
        self._heuristic = None
    
    @property     
    def matrixSrc(self):
        '''
        matrixSrc is the file name of the setup time table (e.g. "matrix.csv")
        '''
        return self._matrixSrc
    @matrixSrc.setter
    def matrixSrc(self, value):
        if value is None: return
        self._matrixSrc = self.directory + value
        self.matrix = Matrix(self._matrixSrc)
    @property 
    def directory(self):
        return self._directory
    @directory.setter
    def directory(self, value):
        self._directory = value
                          
    @property
    def jobSrc(self):
        '''
        jobSrc is a file name for the job data (e.g. "jobs.csv")
        '''
        return self._jobSrc
    @jobSrc.setter
    def jobSrc(self, value):
        if value is None: return
        self._jobSrc = self.directory + value
        self.J += Job.load(jobFile=self._jobSrc)
        
    @property
    def machineSrc(self):
        return self._machineSrc 
    @machineSrc.setter
    def machineSrc(self, value):
        if value is None: return 
        try:
            m = int(value)
            self._machineSrc = '{} auto generated machines'.format(m)
            self.M += [Machine() for i in range(m)] #@UnusedVariable
        except ValueError:
            self._machineSrc = value
            self.M += Machine.load(value)   
       
  
    @property
    def cost(self):
        return Schedule.cost
    @cost.setter
    def cost(self, value):
        Schedule.cost = value
    @property
    def heuristic(self):
        return Schedule.heuristic
    @heuristic.setter
    def heuristic(self, value):
        Schedule.heuristic = value
      
    def finalize(self):
        '''
        Call this function to indicate the configuration of the problem is finished
        '''
        assert self.J and self.M
        self.isFinal = True
        self.current = Schedule(0, len(self.J), len(self.M), self.matrix)
            
    def __repr__(self):
        return ('A scheduling problem with %s jobs and %s machines.\n'  
               'Directory = "%s".\n') % (len(self.J), len(self.M), self.directory)

#===============================================================================
# Machine
#===============================================================================

class Machine(object):
    '''
    classdocs
    '''
    total = 0
    @staticmethod
    def load(filename):
        rv = []
        f = open(filename)
        reader = csv.reader(f)
        headers = reader.next()
        for values in reader:
            d = dict(zip(headers, values))
            rv.append(Machine(**d))
        return rv
    
   

    def __init__(self, **kwargs):
        '''
        Constructor
        '''
        for k in kwargs:
            #Convert to integers if it is possible
            try:
                kwargs[k] = int(kwargs[k])
            except ValueError:
                pass
        
        self.__dict__.update(kwargs)
        self.ID = Machine.total
        Machine.total += 1
        
    def __repr__(self):
        return 'Machine#{}'.format(self.ID)


#===============================================================================
# Job
#===============================================================================

class Job(object):
    '''
    Job class
    @cvar total: keep the count of job objects
    '''
    total = 0
    @staticmethod
    def load(jobFile):
        rv = []
        f = open(jobFile, 'r')
        reader = csv.reader(f)
        headers = reader.next()
        for values in reader:
            d = dict(zip(headers, values))
            rv.append(Job(**d))
        f.close()
        return rv
        
    def __init__(self, **kwargs):
        '''
        Constructor
        '''
        for k in kwargs:
            #Convert to integers if it is possible
            try:
                kwargs[k] = int(kwargs[k])
            except ValueError:
                pass
            
        self.__dict__.update(kwargs)
        self.ID = Job.total
        Job.total += 1

    
    def __repr__(self):
        return 'Job#{}'.format(self.ID)
    
    def __eq__(self, other):
        return self.ID == other.ID
    

#===============================================================================
# Event
#===============================================================================
class Event:
    '''
    An event is a scheduled job represented by a 3-tuple (jobID, start, end)
    '''
    def __init__(self, job, start):
        self.job = job
        self.start = start
        self.end = self.start + self.job.PT 
        

    def __repr__(self):
        return '<{},{},{}>'.format(self.job.ID, self.start, self.end)
   
#===============================================================================
# Schedule
#===============================================================================       
class Schedule(object):
    '''
    A schedule contains events.
    @ivar line: events grouped by the machine
    @ivar ct: completion times for each job
    @ivar line: line[n] represents the events on Machine n
    @ivar clock: an integer that represents the current time of the schedule
    @ivar matrix: the setup matrix if presented
    @type matrix: L{matrix}
    ''' 
    sp = None                   
    def __init__(self, clock=0, nj=0, nm=0, matrix=None):
        self.nj, self.nm = (nj, nm)
        self.ct = [-1 for i in xrange (self.nj)] #@UnusedVariable
        self.line = [None for i in xrange(self.nm)] #@UnusedVariable
        self.numScheduled = 0
        self.clock = clock
        self.matrix = matrix
        self.makespan = clock
  

    def getCopy(self):
        '''
        Make a new copy of the schedule
        '''
        #Copy the clock
        newSch = Schedule(self.clock, self.nj, self.nm, self.matrix)
        for j in xrange(self.nm):
            try:
                newSch.line[j] = [event for event in self.line[j]]
            #when line[j] is None
            except TypeError:
                continue
        newSch.ct = list(self.ct)
        newSch.makespan = max(newSch.ct)
        newSch.numScheduled = self.numScheduled
        return newSch   
        
    def addEvent(self, job):
        '''
        add an event to the schedule
        @param job: the job reference to create the event from
        clock time
        '''
        if self.matrix: 
            e = Event(job, start=self.clock + self.getSetupTime(job))
        else:
            e = Event(job, start=self.clock)
        try:
            self.line[job.REQ].append(e) 
        except AttributeError:
            self.line[job.REQ] = [e]
        self.ct[job.ID] = e.end  
        if e.end > self.makespan:
            self.makespan = e.end
        self.numScheduled += 1
            
    def allocate(self, jobs):
        '''
        @param jobs: the set of jobs to be assigned
        @return: a new schedule with the jobs allocated
        '''
        successor = self.getCopy()   
        if isinstance(jobs, Job):
            successor.addEvent(jobs)
        else:     
            for job in jobs:
                successor.addEvent(job)
        return successor
    
    def process(self, tick=None):
        if tick == None:
            if self.clock == self.slot(): 
                return 'Deadlock'
            self.clock = self.slot()
        else:
            self.clock += tick
    
    def getIdle(self, m):
        if not self.line[m.ID]: return 0
        idle = 0
        for i in range(1, len(self.line[m.ID])):
            idle += self.line[m.ID][i] - self.line[m.ID][i - 1]
        
        if self.line[m.ID][-1].end < self.clock:
            idle += self.clock - self.line[m.ID][-1].end
        return idle
            
    
    def getSetupTime(self, j):
        if self.line[j.REQ] is not None:
            #get the last job scheduled on Machine m.
            last_job = self.line[j.REQ][-1].job.ID
            return self.matrix.get(last_job, j.ID)
        else:#if no jobs are schedule on Machine m yet.
            return self.matrix.get(0, j.ID)
    
    def getDimension(self):
        return (self.nj, self.nm)
               
   
    
    def slot(self):
        '''
        @return: the point of time when a slot (any machine) will be available,
        that is, pick the machine whose last event will finish the earliest
        '''
        candidates = [self.line[m][-1].end for m in xrange(self.nm) 
                         if self.line[m] and self.line[m][-1].end > self.clock]
        return min(candidates or [self.clock])

    
    def isEnabled(self, job):
        '''
        @return: if a job is enabled
        '''
        return (not self.isCompleted(job)) and self.isIndependent(job) and self.isMachineReady(job)
    
    
    def isCompleted(self, job):
        '''
        @return: if job is waiting (not in the schedule)
        @rtype: C{bool}
        '''  
        return True if self.ct[job.ID] >= 0 else False
    
    def isIndependent(self, job):
        pre = job.PREC
        if pre == '':return True
        return 0 < self.ct[pre] <= self.clock
    
    
    def isMachineReady(self, job):
        '''
        @return: if a required machine of job  is free.
        the machine is free when its line is None or last event has finished
        '''
        m = job.REQ
        return True if self.line[m] is None or self.line[m][-1].end <= self.clock else False
    
 
    def isGoal(self):
        '''
        Return True if a schedule is a goal: all jobs are finished
        '''
        return self.numScheduled == self.nj and self.clock == self.slot()
        
    
    def cost(self):
        '''
        Default cost function returns clock value
        '''
        return self.makespan
    
    def heuristic(self):
        '''
        Default heuristic function return 0
        '''
        return 0
    
    def evaluation(self):
        return self.heuristic() + self.cost()
        
    def events(self):
        return '\n'.join(map(str, self.line))
    
    def isSame(self, other):
        '''
        @return: if two schedules are exactly the same
        '''
        return self.events() == other.events()
    
    def __repr__(self):
        '''
        String representation of a schedule
        '''
        rv = '(evaluation,cost,heuristic)={score}\n'.format(
            score=(self.evaluation(), self.cost(), self.heuristic())
            )
        return rv

    
    def __cmp__(self, other):
        '''
        Compare two schedule by their evaluation
        '''
        return cmp(self.evaluation(), other.evaluation())
#===============================================================================
# 
#    @staticmethod
#    def make(array, sp):
#        '''
#        Make a schedule from a two dimension arrays
#        '''
#        nj, nm = len(sp.J), len(sp.M)
#        rv = Schedule(0, nj, nm, sp.matrix)
#        col = [0 for i in range(nm)]
#        #continue as long as any machine has more jobs to schedule
#        while any(col[m] < len(array[m]) for m in range(nm)):
#            for m in range(nm):
#                if col[m] >= len(array[m]):
#                    #skip the machine that is already fully scheduled
#                    continue
#                job = sp.J[array[m][col[m]]]
#                if rv.isEnabled(job):
#                    rv.addEvent(job)
#                    col[m] += 1
#                else:
#                    #wait
#                    pass
#            if rv.process() == 'Deadlock': 
#                return None
#        return rv
#    
#    def getArray(self):
#        return [[e.job.ID for e in self.line[m]] for m in xrange(self.nm)]
# 
#    def getNeighbors(self, sp, machine=None):
#        if machine is not None:
#            machines = (machine,)
#        else:
#            machines = xrange(self.nm)
#        array = self.getArray()
#        for m in machines:
#            i, j = 0, 1
#            while j < len(array[m]):
#                #swap
#                array[m][i], array[m][j] = array[m][j], array[m][i]
#                yield Schedule.make(array, sp)
#                #swap back
#                array[m][i], array[m][j] = array[m][j], array[m][i]
#                i, j = i + 1, j + 1
#                
#===============================================================================
